/* 算法实现模板 */
import AlgorithmBase from "./AlgorithmBase";
import React from "react";
import ReactDom from 'react-dom';
import { loadDataset, txtToMatrix , mdRender} from "../../../common/Common";
import MatView2D, {createRandomPointStyleDict} from "../../../visual/MatView2D";
import Mat from "../../../common/Mat";

import 'katex/dist/katex.min.css';
import TDom from "../../../common/TDom";
import TCommon from "../../../common/TCommon";
import TMatrix from "../../../common/TMatrix";
import { showMathStringInTable } from "../../../visual/Table2D";


class SMO extends AlgorithmBase{
  constructor(){
    super('SMO');

    this.model          = {toler:10e-3, C:0.6}

    this.descriptionUrl = `${this.homepage}/data/slm/ch7/7.5/README.md`;
    this.datasetUrl     = `${this.homepage}/data/slm/ch7/7.5/dataset.txt`;
  }

  init(){ 
    this.reset();

    this.initTrainDS();
  }

  reset(){
    this.dataset = undefined;
    this.trainDS = undefined;
    this.model   = {toler:10e-3, C:0.6}

    this.clear();

    this.initDescription();
    this.updateModel();
  }
  
  // 模型UI
  initModelUI(){
    
    this.addLabel(this.domElementList.model, '惩罚参数:');
    this.Input = this.addFloatInput(
      this.domElementList.model, this.model.C, 
      (event)=>{
        const val = parseFloat(event.target.value);
        if(val<0){
          this.model.C = 0
          event.target.value = 0
        }
        else this.model.C = val;
        this.updateModel();
    });

    this.addLabel(this.domElementList.model, '   ');

    this.addLabel(this.domElementList.model, ' 精度:');
    this.tolerInput = this.addFloatInput(
      this.domElementList.model, this.model.toler, 
      (event)=>{
        const val = parseFloat(event.target.value);
        this.model.toler = val;
        this.updateModel();
    });

    this.modelDiv = this.addDiv(this.domElementList.model);
  }

  // 渲染模型到页面
  updateModel(w, b){

    if(w === undefined) w = new Mat.zeros([1, 2]);
    if(b === undefined) b = 0;

    const modelString = String.raw
    `$$ \begin{aligned} C&:=${this.model.C} \\ \epsilon&:=${this.model.toler}\end{aligned} $$`

    mdRender(`${modelString}`, this.outputDiv);
    // 显示数据集
    if(this.trainDS){
      ReactDom.unmountComponentAtNode(this.inputDiv);
      ReactDom.render(
        <React.StrictMode>
          <MatView2D 
            Mat     = {new TMatrix.Mat(this.trainDS.Mat)}
            Labels  = {this.trainDS.Labels}
            XRange  = {[0, 10]}
            YRange  = {[-5, 5]}
            Separator = { {type:'line', w:w.toVector(), b:b} }
            PointStyleDict = {this.pointStyleDict}
          />
        </React.StrictMode>,
        this.inputDiv
      );
    }
  }

  // 输入数据UI
  initInputUI(){

    // 文件选择器
    const fileSelector      = document.createElement('input');
    fileSelector.type       = 'file';
    fileSelector.hidden     = true;
    fileSelector.onchange   = this.handleFileChange.bind(this);   
    
    // this.addButton(this.domElementList.input, "获取服务器数据", this.initTrainDS.bind(this));
    TDom.createButton(this.domElementList.input, "获取服务器数据", this.initTrainDS.bind(this));
    this.addButton(this.domElementList.input, "使用本地数据", this.openFileSelector.bind(this));
    this.domElementList.input.appendChild(fileSelector);
    this.addButton(this.domElementList.input, "训练", this.train.bind(this));
    this.playButton = this.addButton(this.domElementList.input, "训练动画", this.trainAnimation.bind(this));
    this.playButton.hidden = true;

    this.labelTrainTips = this.addLabel(this.domElementList.input, '');
    this.inputDiv       = this.addDiv(this.domElementList.input);

    this.fileSelector  = fileSelector;
  }

  // 输出UI
  initOutputUI(){
    this.outputDiv   = this.addDiv(this.domElementList.output);
    this.trainLogDiv = this.addDiv(this.domElementList.output);
  }

  // 获取描述md文件
  initDescription(){
    TDom.getDataFromUrl(this.descriptionUrl, (txt)=>{
      mdRender(txt, this.descriptionDiv);
    })

  }

  // 获取服务器数据集
  initTrainDS(){
    this.prepare(this.datasetUrl);
  }

  // 准备数据训练数据
  prepare(url){
    TDom.getDataFromUrl(url, 
      (txt)=>{
      this.dataset = txtToMatrix(txt);
      this.trainDS = TCommon.loadDataset(this.dataset);
      this.pointStyleDict = createRandomPointStyleDict(this.trainDS.Labels);
      this.updateModel();
    })
  }

  openFileSelector(){
    this.fileSelector.click();
  }
  // 获取文件路径加载
  handleFileChange(event){
    const files = event.target.files;

    for(let i = 0, il = files.length; i < il; i++){
      let reader = new FileReader();
      reader.onload = (e) => {      
        this.prepare(e.target.result);
        if(i === files.length - 1){
          event.target.value = '';
        }
      };
      reader.readAsDataURL(files[i])
    }
  }

  clipAlpha(aj, H, L){
    if (aj > H)
    {
      aj = H;
    }
    else if (L > aj)
    {
      aj = L
    }
    return aj;
  }

  // 训练
  train(){
    this.playButton.hidden = true;
    const trainDS = this.trainDS;
    if(!trainDS) return;

    let X = new Mat.Mat(trainDS.Mat);
    let y = new Mat.Mat(trainDS.Labels);
    var w = new Mat.zeros([1, 2]);
    var b = 0.0;
    var toler = parseFloat(this.model.toler);
    var C = parseFloat(this.model.C);
    var alphas = new Mat.zeros([1,y.length])
    alphas = alphas[0]

    var iter_num = 0;
    var iter = 500
    var trainHistory = [];    // 训练历史
    var trainLog = [];
    trainLog.push(["迭代次数","优化次数","w","b"])
    

    for(var i=0;i<y.length;i++){
      y[i]=y[i][0]
    }

    while(iter_num < iter){
      var alphaPairsChanged = 0;
      for(var i=0;i<y.length;i++){
        //计算误差Ei
        var fxi = 0;
        for (var k=0;k<y.length;k++)
        {
          var xi =  parseFloat(X[k][0]*X[i][0]+X[k][1]*X[i][1]);
          fxi += parseFloat(alphas[k] * y[k]) * parseFloat(xi);
        }
        fxi += b;
        var Ei = parseFloat(fxi) - parseFloat(y[i]);

        //优化alpha，更设定一定的容错率。
        if (((y[i]*Ei < -toler) && (alphas[i] < C)) || ((y[i]*Ei > toler) && (alphas[i] > 0)))
        {
          while (1)
          {
            var j = Math.round(Math.random()*(y.length-1));
            if (j != i )
            {
              break;
            }
          }

          //计算Ej
          var fxj = 0;
          for (var k=0;k<y.length;k++)
          {
            var xj =  parseFloat(X[k][0]*X[j][0]+X[k][1]*X[j][1]);
            fxj += parseFloat(alphas[k] * y[k]) * parseFloat(xj);
          }
          fxj += b;
          var Ej = parseFloat(fxj) - parseFloat(y[j]);
          //保存a_old
          var alphaIold = alphas[i];
          var alphaJold = alphas[j];

          //计算上界和下界
          if (y[i] != y[j])
          {
            var L = Math.max(0, alphas[j] - alphas[i]);
            var H = Math.min(C, C + alphas[j] - alphas[i]);
          }
          else
          {
            var L = Math.max(0, alphas[j] + alphas[i] - C);
            var H = Math.min(C, alphas[j] + alphas[i]);
          }
          if (L == H)
          {
            continue;
          }

          //计算eta
          var eta = 2.0 * parseFloat(X[i][0]*X[j][0]+X[i][1]*X[j][1]) - parseFloat(X[i][0]*X[i][0]+X[i][1]*X[i][1]) - parseFloat(X[j][0]*X[j][0]+X[j][1]*X[j][1]);
          if (eta >= 0)
          {
            continue;
          }

          //更新/修剪 a_j
          alphas[j] -= y[j]*(Ei - Ej)/eta;
          alphas[j] = this.clipAlpha(alphas[j], H, L);
          if (Math.abs(alphas[j] - alphaJold) < 0.00001)
          {
            continue;
          }

          //更新 a_i
          alphas[i] += y[j]*y[i]*(alphaJold - alphas[j]);

          //更新 b_1,b_2
          var b1 = b - Ei- y[i]*(alphas[i]-alphaIold)*parseFloat(X[i][0]*X[i][0]+X[i][1]*X[i][1]) - y[j]*(alphas[j]-alphaJold)*parseFloat(X[j][0]*X[i][0]+X[j][1]*X[i][1]);
          var b2 = b - Ej- y[i]*(alphas[i]-alphaIold)*parseFloat(X[i][0]*X[j][0]+X[i][1]*X[j][1]) - y[j]*(alphas[j]-alphaJold)*parseFloat(X[j][0]*X[j][0]+X[j][1]*X[j][1]);

          //更新b
          if ((0 < alphas[i]) && (C > alphas[i]))
          {
            b = b1;
          }
          else if ((0 < alphas[j]) && (C > alphas[j]))
          {
            b = b2;
          }
          else
          {
            b = (b1+b2)/2.0;
          }

          //更细w
          var w1 = 0;
          var w2 = 0;
          for (var i=0;i<y.length;i++)
          {
            w1 += alphas[i] * y[i] * X[i][0];
            w2 += alphas[i] * y[i] * X[i][1];
          }
          w[0][0] = w1
          w[0][1] = w2

          //统计优化次数
          alphaPairsChanged += 1;
          trainLog.push([`${iter_num}`, `${alphaPairsChanged}`, `${w[0]}`, `${b}`])
        }
      }
      if(alphaPairsChanged != 0) iter++

      trainHistory.push({ errPoint:iter_num, w:w, b:b, time:new Date()} );
      iter_num++
    }

    var index = new Array();
    for (var i=0;i<y.length;i++)
    {
      if (alphas[i]>0)
      {
        index.push(i);
      }
    }
    

    this.updateModel(w, b);
    this.labelTrainTips.innerText = '训练完毕';
    if(this.trainLogDiv) this.trainLogDiv.innerHTML = '';
    showMathStringInTable(this.trainLogDiv, trainLog);

    this.playButton.hidden = false;
    this.trainHistory = trainHistory;
                
  }

  trainAnimation(){
    if(this.trainHistory && !this.animation){
      this.trainHistoryIndex = 0;
      this.animation  = setInterval(this.playHistory.bind(this), 100);
    }
  }

  playHistory(){
    if(this.trainHistoryIndex < this.trainHistory.length){
      const lastest = this.trainHistory[this.trainHistoryIndex];
      this.updateModel(lastest.w, lastest.b);
      this.labelTrainTips.innerText = `训练时间:${lastest.time.toString()}`;
      this.trainHistoryIndex += 1;
    }else{
      clearInterval(this.animation);
      this.animation = '';
      this.labelTrainTips.innerText = `播放完毕`;
    }
  }
}


export default SMO;